iT邦幫忙

2026 iThome 鐵人賽

DAY 9
1
AI Engineering

30天用 Claude Code + LangGraph 實作個人化 AI 學習教練系列 第 9

Day 9:連接 Ollama 本機模型 - 拿到結構化回應

  • 分享至 

  • xImage
  •  

昨天我們確認了 Ollama 能連得上,拿到一句話的回答。但一句話沒辦法直接存進資料庫,也沒辦法讓程式知道「這句話裡哪個是主題、哪個是預估時數」。

今天要解決這件事:讓本機模型直接回傳「照格式排好的資料」,而不是一段要人工解析的文字。這是 Day 13 寫 Planner Agent 的地基,先在這裡把「怎麼拿到乾淨的結構化資料」練熟。

為什麼要「結構化回應」

直接請模型用文字回答,你會拿到類似這樣的東西:

我建議你今天學習 EC2 的基礎概念,因為這是後續服務的基礎,大概需要 2 小時。

這對人很好讀,但程式沒辦法直接用。程式需要的是:

{"topic": "EC2 基礎概念", "reason": "後續服務的基礎", "estimated_hours": 2}

想像成點餐:跟服務生說「我要一份套餐,附薯條跟可樂」,廚房聽得懂但沒辦法直接下單;點餐機把它拆成「主餐、附餐、飲料」三個欄位,廚房才能照著做。結構化回應就是把模型的答案拆進固定欄位,讓後面的資料庫、API 都能直接使用。

今天用 LangChain 包一層

Day 10 開始要用 LangGraph 組出 Agent 的流程,LangGraph 本身是建立在 LangChain 之上的。今天先透過 langchain-ollama 提供的 ChatOllama,熟悉這一層的用法,Day 10 寫 Node 的時候會直接沿用今天的東西。

ChatOllama 底層還是呼叫本機的 Ollama 服務,只是多包了一層方便串接 LangGraph 和結構化輸出的介面。

小提醒:開源模型在「照格式輸出」這件事上,穩定度通常不如大型商用模型,llama3.1:8b 已經支援工具呼叫(tool calling),能勝任結構化輸出,但偶爾還是會輸出格式跑掉。這也是今天要順便加上重試機制的原因。


實作步驟

步驟1:安裝套件

cd backend
python -m pip install langchain langchain-ollama

pydantic 已經隨 FastAPI 裝好了,不用再裝一次。

驗證:

python -c "from langchain_ollama import ChatOllama; print('安裝成功')"

步驟2:定義回應格式

用 Pydantic 描述「希望模型回什麼欄位」,Field 裡的 description 會被當成提示的一部分,告訴模型每個欄位該填什麼。

檔案位置: backend/schemas.py
狀態: 修改檔案(在 Day 7 的內容後面加上這段)
用途: 新增本機模型結構化回應會用到的格式
依賴: pydantic

class StudyAdvice(BaseModel):
    """模型針對今日學習給出的建議"""
    topic: str = Field(description="今天建議學習的主題")
    reason: str = Field(description="為什麼推薦這個主題")
    estimated_hours: float = Field(gt=0, description="預估需要花費的時數")

步驟3:寫 ollama_client.py

檔案位置: backend/ollama_client.py
狀態: 新增檔案
用途: 封裝 Ollama 本機模型呼叫,提供結構化回應與重試機制
依賴: langchain-ollama, schemas

from langchain_ollama import ChatOllama
from schemas import StudyAdvice

# temperature 調低一點,讓結構化輸出更穩定、不容易亂跑格式
_llm = ChatOllama(model="llama3.1:8b", temperature=0.3)

# with_structured_output:讓輸出直接符合 StudyAdvice 的格式
# with_retry:輸出格式偶爾跑掉時自動重試,最多 3 次
_structured_llm = _llm.with_structured_output(StudyAdvice).with_retry(
    stop_after_attempt=3
)


def get_study_advice(user_message: str) -> StudyAdvice:
    """根據使用者輸入,請本機模型給出結構化的學習建議"""
    return _structured_llm.invoke(user_message)

with_structured_output(StudyAdvice) 是關鍵:LangChain 會把 StudyAdvice 的欄位定義轉成模型看得懂的格式要求,模型回傳後也會自動解析、驗證成 StudyAdvice 物件,不用自己寫 JSON 解析和格式檢查。

步驟4:寫測試腳本

檔案位置: backend/test_ollama_client.py
狀態: 新增檔案
用途: 驗證 ollama_client.py 能重複拿到正確格式的結構化回應
依賴: ollama_client

from ollama_client import get_study_advice


def main() -> None:
    """呼叫兩次,確認結構化輸出穩定、可重複"""
    for i in range(1, 3):
        advice = get_study_advice(
            "我正在準備 AWS Solutions Architect 認證,目前是初學者,"
            "已經花了一週讀 EC2,接下來建議學什麼?"
        )
        print(f"第 {i} 次呼叫:")
        print(f"  主題:{advice.topic}")
        print(f"  原因:{advice.reason}")
        print(f"  預估時數:{advice.estimated_hours}")


if __name__ == "__main__":
    main()

執行:

python test_ollama_client.py

第一次執行可能要等模型「暖機」(Ollama 第一次呼叫某個模型時要把它載進記憶體),會比第二次呼叫慢一些。應該看到兩次呼叫都回傳有 topicreasonestimated_hours 三個欄位的乾淨結果,即使兩次問的內容一樣,答案的用詞也可能不完全一樣,但格式一定固定。

步驟5:確認驗證有生效(選做)

advice 是一個 StudyAdvice 物件,不是字典,所以打錯欄位名稱會被 Python 直接抓到。這步只是驗證用,不用寫進任何檔案,跟著下面的步驟在終端機操作就好。

1. 確認在 backend/ 目錄下

打開終端機,確認路徑在 backend/(跟 ollama_client.py 同一層):

cd backend

2. 進入互動式 Python

輸入:

python

按 Enter 後,畫面會變成這樣,代表已經進入互動模式,等你輸入指令:

>>>

3. 匯入剛剛寫好的函式

>>> 後面貼上這一行,按 Enter:

from ollama_client import get_study_advice

沒有出現任何錯誤訊息就是成功了。

4. 呼叫一次,拿到結果

繼續貼上這一行,按 Enter(這一步會實際呼叫 Ollama,需要等幾秒):

advice = get_study_advice("推薦我今天學什麼")

5. 故意打錯欄位名稱

貼上這一行,注意 estimated_hour 少了最後的 s,這是刻意的:

advice.estimated_hour

按 Enter 後應該看到:

AttributeError: 'StudyAdvice' object has no attribute 'estimated_hour'

看到這個錯誤,就代表 Pydantic 定義的格式是真的在把關資料型別,而不是只是好看的型別提示。如果改打正確的欄位名稱 advice.estimated_hours(沒有打錯),會正常印出一個數字,不會報錯。

6. 離開互動模式

驗證完輸入:

exit()

回到一般的終端機畫面即可,不需要保留或刪除任何檔案。


常見問題

with_structured_output 回傳的內容跟預期不符,甚至報錯格式錯誤

開源模型比商用模型更容易「不照格式」。把 Fielddescription 寫得更具體,例如把「主題」改成「一個具體的技術概念名稱,不要用整個章節當作主題」,通常就能改善。如果還是不穩定,可以把 temperature 調更低(例如 0.1),輸出會更保守、格式更穩定。

重試了 3 次還是失敗

先確認 Day 8 的 test_ollama.py 還能正常跑,排除 Ollama 服務本身沒開或模型沒下載的問題。如果基本連線沒問題,單純是結構化輸出不穩定,可以考慮換 qwen2.5:7b 試試看,不同模型對格式的服從度不一樣。

為什麼不直接請模型輸出 JSON 文字就好?

可以,但那樣要自己寫 json.loads()、自己檢查欄位齊不齊全、型別對不對,模型只要漏一個逗號整段就解析失敗。with_structured_output 把這些都包好了,而且 Day 10 開始要用 LangGraph 組 Agent 流程,ChatOllama 正好是 LangGraph 期待的介面,兩邊銜接不用額外轉換。

ModuleNotFoundError: No module named 'langchain_ollama'

確認用 python -m pip install langchain langchain-ollama 裝在跟執行腳本相同的 Python 環境。

可以換成別的欄位嗎?

可以,StudyAdvice 只是範例格式。Day 13 寫 Planner Agent 時,會定義一個更大的 Schema 來裝一整週的計畫,做法完全一樣,只是欄位更多。


進度回顧

今天把 Ollama 升級成「能回傳結構化資料」的版本。寫了 ollama_client.py,用 Pydantic 定義好格式,加上自動重試,兩次呼叫都能拿到格式一致、能直接被程式使用的結果。

系統現在是這樣的:

Day 1 ✓ 產品定義完成
Day 2 ✓ 開發環境準備
Day 3 ✓ 專案架構設計
Day 4 ✓ 資料庫設計
Day 5 ✓ SQLite 資料庫建置
Day 6 ✓ FastAPI 基礎
Day 7 ✓ 使用者檔案 API
Day 8 ✓ 理解 LLM Agent 的本質
Day 9 ✓ 連接 Ollama 本機模型(今天)
Day 10 ⬜ LangGraph 最小範例

ollama_client.py 會是接下來所有 Agent 的共同基礎。明天(Day 10)要把它接進 LangGraph,組出第一個真正會跑的 Agent 流程:輸入 → 本機模型 → 輸出。這是這兩週的難點,明天多留一點時間比較保險。


上一篇
Day 8:理解 LLM Agent 的本質
下一篇
Day 10:LangGraph 最小範例 - 組出第一個 Agent
系列文
30天用 Claude Code + LangGraph 實作個人化 AI 學習教練10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
tsengyulun
iT邦新手 5 級 ‧ 2026-09-23 23:00:01

今天沒有提醒我

我要留言

立即登入留言